package Dungeon;

import java.math.BigDecimal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;

import be.kuleuven.cs.som.annotate.Basic;
import Border.Direction;
import Coordinates.Coordinates;
import Predicate.Predicate;
import Square.*;


public abstract class RegularDungeon<T extends Square> extends Dungeon<T>{
	
	public RegularDungeon(long xDimension, long yDimension, long zDimension)
			throws IllegalArgumentException {
		super(xDimension, yDimension, zDimension);
	}
	

	/**
	 * Returns the square from the given coordinates in this dungeon.
	 * @param 	x
	 * 			X-Coordinate to get the square from.
	 * @param 	y
	 * 			Y-Coordinate to get the square from.
	 * @param 	z
	 * 			Z-Coordinate to get the square from.
	 * @note	Returns null if there's no square at given coordinates
	 * 
	 */
	@Basic
	public T getSquare(long x, long y, long z)
	{
		return squares.get( new Coordinates(x,y,z) );
	}
	
	/**
	 * 
	 * @param 	square
	 * 			The square to add
	 * @param 	x
	 * 			The x coordinate for the new square
	 * @param 	y	
	 * 			The y coordinate for the new square
	 * @param 	z
	 * 			The z coordinate for the new square
	 * @throws 	IllegalStateException
	 * 			Throws an IllegalStateException if the percentage of slippery squares in the dungeon would be larger then 20% after the given square is added
	 * 			| calculateSlipperySquaresPercentage(square) > 0.20
	 * @throws	IllegalArgumentException
	 * 			Throws an IllegalArgumentException if the given square is not effective
	 * 			| square == null
	 * @throws 	IllegalArgumentException
	 * 			Throws an IllegalArgumentException if the given coordinates aren't valid for this dungeon
	 * 			| !Coordinates.areValidCoordinates(this, x, y, z)
	 * @throws	IllegalArgumentException
	 * 			The square already belongs to this dungeon
	 * 			| isPartOfDungeon(square)
	 * @post	The square is added to the dungeon
	 * 			| new.getSquare(x,y,z) == square
	 * @effect	The temperature and humidity for the space that the newly added square is in will be recalculated
	 * 			| square.mergeSpace()
	 */
	public void addSquare(T square, long x, long y, long z) throws IllegalStateException, IllegalArgumentException
	{
		if(square == null)
			throw new IllegalArgumentException();
		else if(calculateSlipperySquaresPercentage(square).compareTo(new BigDecimal(0.20)) > 0)
			throw new IllegalStateException();
		else if(!Coordinates.areValidCoordinates(this, x, y, z))
			throw new IllegalArgumentException();
		else if(isPartOfDungeon(square))
			throw new IllegalArgumentException();
		else
		{
			squares.put( new Coordinates(x,y,z) , square );
			
			mergeWithSurroundingSquares(square, x, y, z);
			mergeWithSquaresFromOtherDungeons(square, x, y, z);
			
			if(square.getClass() != Rock.class )
				((DefaultSquare) square).mergeSpace();
		}
	}

	/**
	 * 
	 * @param 	square
	 * 			The square to add
	 * @param 	x
	 * 			The x coordinate for the new square
	 * @param 	y	
	 * 			The y coordinate for the new square
	 * @param 	z
	 * 			The z coordinate for the new square
	 * @effect	The newly added square is merged with all surrounding squares, including the ones from other dungeons that are in the same composited dungeon
	 * 			then the square's containing dungeon.
	 * 			root = this.getRootDungeon();
	 *			| if(root != null)
	 *			| {
	 *			| 	origin = getDistanceToRootOrigin();
	 *			| 	if( root.getSquare(origin.getX()+x - 1, origin.getY()+y, origin.getZ()+z) != null )
	 *			|		square.mergeBorders(root.getSquare(origin.getX()+x - 1, origin.getY()+y, origin.getZ()+z), Direction.WEST);
	 *			|	if( root.getSquare(origin.getX()+x+1, origin.getY()+y, origin.getZ()+z) != null )
	 *			|		square.mergeBorders(root.getSquare(origin.getX()+x + 1, origin.getY()+y, origin.getZ()+z), Direction.EAST);
	 *			|	if( root.getSquare(origin.getX()+x, origin.getY()+y+1, origin.getZ()+z) != null )
	 *			|		square.mergeBorders(root.getSquare(origin.getX()+x, origin.getY()+y+1, origin.getZ()+z), Direction.SOUTH);
	 *			|	if( root.getSquare(origin.getX()+x, origin.getY()+y-1, origin.getZ()+z) != null )
	 *			|		square.mergeBorders(root.getSquare(origin.getX()+x , origin.getY()+y-1, origin.getZ()+z), Direction.NORTH);
	 *			|	if( root.getSquare(origin.getX()+x, origin.getY()+y, origin.getZ()+z+1) != null )
	 *			|		square.mergeBorders(root.getSquare(origin.getX()+x, origin.getY()+y, origin.getZ()+z+1), Direction.CEILING);
  	 *			|	if( root.getSquare(origin.getX()+x, origin.getY()+y, origin.getZ()+z-1) != null )
	 *			|		square.mergeBorders(root.getSquare(origin.getX()+x , origin.getY()+y, origin.getZ()+z-1), Direction.FLOOR);
	 *			| }
	 * 			
	 */
	private void mergeWithSquaresFromOtherDungeons(T square, long x, long y, long z) {
		CompositeDungeon<? super T> root = this.getRootDungeon();
		if(root != null && root.hasDungeon(this))
		{
			Coordinates origin = this.getDistanceToRootOrigin();
			if( root.getSquare(origin.getX()+x - 1, origin.getY()+y, origin.getZ()+z) != null )
				square.mergeBorders(root.getSquare(origin.getX()+x - 1, origin.getY()+y, origin.getZ()+z), Direction.WEST);
			if( root.getSquare(origin.getX()+x+1, origin.getY()+y, origin.getZ()+z) != null )
				square.mergeBorders(root.getSquare(origin.getX()+x + 1, origin.getY()+y, origin.getZ()+z), Direction.EAST);
			if( root.getSquare(origin.getX()+x, origin.getY()+y+1, origin.getZ()+z) != null )
				square.mergeBorders(root.getSquare(origin.getX()+x, origin.getY()+y+1, origin.getZ()+z), Direction.SOUTH);
			if( root.getSquare(origin.getX()+x, origin.getY()+y-1, origin.getZ()+z) != null )
				square.mergeBorders(root.getSquare(origin.getX()+x , origin.getY()+y-1, origin.getZ()+z), Direction.NORTH);
			if( root.getSquare(origin.getX()+x, origin.getY()+y, origin.getZ()+z+1) != null )
				square.mergeBorders(root.getSquare(origin.getX()+x, origin.getY()+y, origin.getZ()+z+1), Direction.CEILING);
			if( root.getSquare(origin.getX()+x, origin.getY()+y, origin.getZ()+z-1) != null )
				square.mergeBorders(root.getSquare(origin.getX()+x , origin.getY()+y, origin.getZ()+z-1), Direction.FLOOR);
		}
	}

	/**
	 * 
	 * @param 	square
	 * 			The square that is going to be added to the dungeon
	 * @param 	x
	 * 			The x-coordinate for the newly added square
	 * @param 	y
	 * 			The y-coordinate for the newly added square
	 * @param 	z
	 * 			The z-coordinate for the newly added square
	 * 
	 * @effect	The square's borders are merged with the borders of its surrounding squares
	 * 			| if( getSquare(x - 1, y, z) != null )
	 *			| 	square.mergeBorders(getSquare(x - 1, y, z), Border.Direction.WEST)
	 *			| if( getSquare(x+1, y, z) != null )
	 *			|	square.mergeBorders(getSquare(x + 1, y, z), Border.Direction.EAST)
	 *			| if( getSquare(x, y+1, z) != null )
	 *			|	square.mergeBorders(getSquare(x, y+1, z), Border.Direction.SOUTH)
	 *			| if( getSquare(x, y-1, z) != null )
	 *			|	square.mergeBorders(getSquare(x , y-1, z), Border.Direction.NORTH)
	 *			| if( getSquare(x, y, z+1) != null )
	 *			|	square.mergeBorders(getSquare(x, y, z+1), Border.Direction.CEILING)
	 *			| if( getSquare(x, y, z-1) != null )
	 *			|	square.mergeBorders(getSquare(x , y, z-1), Border.Direction.FLOOR)
	 */
	private void mergeWithSurroundingSquares(T square, long x, long y, long z) {
		if( getSquare(x - 1, y, z) != null )
			square.mergeBorders(getSquare(x - 1, y, z), Direction.WEST);
		if( getSquare(x+1, y, z) != null )
			square.mergeBorders(getSquare(x + 1, y, z), Direction.EAST);
		if( getSquare(x, y+1, z) != null )
			square.mergeBorders(getSquare(x, y+1, z), Direction.SOUTH);
		if( getSquare(x, y-1, z) != null )
			square.mergeBorders(getSquare(x , y-1, z), Direction.NORTH);
		if( getSquare(x, y, z+1) != null )
			square.mergeBorders(getSquare(x, y, z+1), Direction.CEILING);
		if( getSquare(x, y, z-1) != null )
			square.mergeBorders(getSquare(x , y, z-1), Direction.FLOOR);
	}
	
	/**
	 * Method that removes the square rom the square list.
	 *  
	 * @param 	x
	 * 			The x-coordinate from the square that will be removed
	 * 
	 * @param 	y
	 * 			The y-coordinate from the square that will be removed
	 * 
	 * @param 	z
	 * 			The z-coordinate from the square that will be removed
	 * 
	 * @post	The square is removed from the dungeon
	 * 			| new.getSquare(x,y,z) == null
	 * 
	 * @note	If there's no square at the given coordinates nothing will change/happen
	 */
	public void removeSquare(long x, long y, long z) 
	{
		squares.remove( new Coordinates(x,y,z) );
	}
	
	/**
	 * Returns a hash map of all squares in this dungeon with coordinates as keys, squares as values
	 */ 
	@Basic
	public HashMap<Coordinates, T> getSquaresMap()
	{
		return squares;
	}
	
	
	/**
	 * A hash map containing all the squares in this dungeon
	 */
	HashMap<Coordinates, T> squares = new HashMap<Coordinates, T>();
	
	/**
	 * 
	 * Calculates the percentage of slippery squares after the given square would be added to the dungeon
	 * 
	 * @return 	Percentage of slippery squares
	 * 			| totalSquares = squares.size() + 1;
	 * 			| slipperySquares = 0
	 *			| if(square.isSlipperMaterial())
	 *			|		slipperySquares++
	 * 			| for each square in getAllSquares()
	 * 			| 	if square.isSlipperMaterial()
	 * 			| 		slipperySquares++
	 * 			| result == slipperySquares / totalSquares
	 */
	public BigDecimal calculateSlipperySquaresPercentage(Square square)
	{
		long totalSquares = squares.size() + 1;
		long slipperySquares = 0;
		if(square.isSlipperMaterial())
			slipperySquares++;
				
		Collection<T> collection = squares.values();
		 
	    Iterator<T> i = collection.iterator();

	    while(i.hasNext())
	      if( i.next().isSlipperMaterial() )
	    	  slipperySquares++;
	    
	    BigDecimal result = new BigDecimal(slipperySquares);
	    result.divide( new BigDecimal(totalSquares) );
	    return result;
	}
	
	
	/**
	 * Method that checks if the given square is part of the dungeon
	 * 
	 * @param 		square
	 * 				Square that is being looked for in the dungeon
	 * 
	 * @return		Returns true if the square is part of the dungeon, and false if not.
	 * 				| result == getAllSquares.containsValue(square)
	 */
	public boolean isPartOfDungeon(T square){
		return getSquaresMap().containsValue(square);
	}
	
	/**
	 * 
	 * Checks whether there's a square in the dungeon at a given set of coordinates
	 * 
	 * @param 	x
	 * 			The x-coordinate
	 * @param 	y
	 * 			The y-coordinate
	 * @param 	z
	 * 			The z-coordinate
	 * @return	Returns true if there's a square at the given coordinates, false if there isn't
	 * 			| result == (getSquare(x, y, z) != null)
	 */
	public boolean hasSquareAt( long x, long y, long z )
	{
		return getSquare(x, y, z) != null;
	}
	
	/**
	 * Returns an iterator 
	 * 
	 * @return	Returns an iterator with all squares from this dungeon
	 * 			| result == getSquaresMap().values().iterator()
	 */
	@Override
	public Iterator<T> getIterator()
	{
		return getSquaresMap().values().iterator();
	}
	
	/**
	 * Returns a filtered iterator
	 * 
	 * @param 	predicate
	 * 			A logical predicate used to filter the squares in this dungeon
	 * @return	Returns a filtered iterator with the squares from this dungeon
	 * 			| squaresToRemove = new ArrayList<T>()
	 *			| collection = squares.values()
	 *   		| for each element in collection
	 *       	|	if (!predicate.apply(element))
	 *       	|		squaresToRemove.add(element)
	 *   		| collection.removeAll(squaresToRemove)
	 *   		| result ==  collection.iterator()
	 */
	@Override
	public Iterator<T> getIterator(Predicate<T> predicate)
	{
		Collection<T> squaresToRemove = new ArrayList<T>();
		Collection<T> collection = squares.values();
	    for (T element: collection) {
	        if (!predicate.apply(element))
	        	squaresToRemove.add(element);
	    }
	    collection.removeAll(squaresToRemove);
	    return collection.iterator();
	}
	
	

}
